package com.neo.tiny.oauth.controller;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.neo.tiny.common.common.ResResult;
import com.neo.tiny.common.constant.KeyValue;
import com.neo.tiny.common.enums.UserTypeEnum;
import com.neo.tiny.common.exception.WebApiException;
import com.neo.tiny.oauth.entity.SysOauth2ApproveDO;
import com.neo.tiny.oauth.entity.SysOauth2ClientDO;
import com.neo.tiny.oauth.enums.LogoutScope;
import com.neo.tiny.oauth.enums.OAuth2GrantTypeEnum;
import com.neo.tiny.oauth.service.Oauth2GrantService;
import com.neo.tiny.oauth.service.Oauth2TokenService;
import com.neo.tiny.oauth.service.SysOauth2ApproveService;
import com.neo.tiny.oauth.service.SysOauth2ClientService;
import com.neo.tiny.oauth.util.OAuth2Utils;
import com.neo.tiny.oauth.vo.open.OAuth2OpenAccessTokenRespVO;
import com.neo.tiny.oauth.vo.open.OAuth2OpenAuthorizeInfoRespVO;
import com.neo.tiny.oauth.vo.open.OAuth2OpenCheckTokenRespVO;
import com.neo.tiny.oauth.vo.token.OauthLogoutReqVO;
import com.neo.tiny.secrity.util.SecurityUtils;
import com.neo.tiny.token.context.TokenAuthentication;
import com.neo.tiny.token.store.AccessTokenInfo;
import com.neo.tiny.token.store.OAuth2AccessToken;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author yqz
 * @Description 提供给外部应用调用为主
 * <p>
 * 一般来说，管理后台的 /admin/* 是不直接提供给外部应用使用，主要是外部应用能够访问的数据与接口是有限的，而管理后台的 RBAC 无法很好的控制。
 * 参考大量的开放平台，都是独立的一套 OpenAPI，对应到【本系统】就是在 Controller 下新建 oauth 包，实现 /open/* 接口，然后通过 scope 进行控制。
 * 另外，一个公司如果有多个管理后台，它们 client_id 产生的 access token 相互之间是无法互通的，即无法访问它们系统的 API 接口，直到两个 client_id 产生信任授权。
 * <p>
 * 考虑到【本系统】暂时不想做的过于复杂，默认只有获取到 access token 之后，可以访问【本系统】管理后台的 /admin/* 所有接口，除非手动添加 scope 控制。
 * scope 的使用示例，可见 {@link OAuth2UserController} 类
 * @CreateDate 2022/11/11 9:04
 */
@Slf4j
@Api(tags = "OAuth2.0 授权")
@RestController
@AllArgsConstructor
@RequestMapping("/open/oauth2")
public class OAuth2OpenTokenController {


    private final SysOauth2ClientService oauth2ClientService;

    private final Oauth2GrantService oauth2GrantService;

    private final SysOauth2ApproveService oauth2ApproveService;

    private final Oauth2TokenService oauth2TokenService;

    /**
     * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
     * <p>
     * 授权码 authorization_code 模式时：code + redirectUri + state 参数
     * 密码 password 模式时：username + password + scope 参数
     * 刷新 refresh_token 模式时：refreshToken 参数
     * 客户端 client_credentials 模式：scope 参数
     * 简化 implicit 模式时：不支持
     * <p>
     * 注意，默认需要传递 client_id + client_secret 参数
     */
    @PostMapping("/token")
    @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【获取】调用")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class),
            @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class),
            @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.baidu.com", dataTypeClass = String.class),
            @ApiImplicitParam(name = "state", value = "状态", example = "1", dataTypeClass = String.class),
            @ApiImplicitParam(name = "username", example = "tiny", dataTypeClass = String.class),
            // 多个使用空格分隔
            @ApiImplicitParam(name = "password", example = "cai", dataTypeClass = String.class),
            @ApiImplicitParam(name = "scope", example = "user_info", dataTypeClass = String.class),
            @ApiImplicitParam(name = "refresh_token", example = "123424233", dataTypeClass = String.class),
    })
    public ResResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
                                                                  @RequestParam("grant_type") String grantType,
                                                                  // 授权码模式
                                                                  @RequestParam(value = "code", required = false) String code,
                                                                  // 授权码模式 - 重定向路径
                                                                  @RequestParam(value = "redirect_uri", required = false) String redirectUri,
                                                                  //授权码模式 - 请求标识
                                                                  @RequestParam(value = "state", required = false) String state,
                                                                  // 密码模式 - 用户名
                                                                  @RequestParam(value = "username", required = false) String username,
                                                                  // 密码模式 - 密码
                                                                  @RequestParam(value = "password", required = false) String password,
                                                                  // 密码模式 - 授权范围
                                                                  @RequestParam(value = "scope", required = false) String scope,
                                                                  // 客户端模式
                                                                  @RequestParam(value = "client_id", required = false) String client_id,
                                                                  @RequestParam(value = "client_secret", required = false) String client_secret,
                                                                  @RequestParam(value = "refresh_token", required = false) String refreshToken) {

        List<String> scopes = OAuth2Utils.buildScopes(scope);

        // 1.1 校验授权范围
        OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
        if (Objects.isNull(grantTypeEnum)) {
            throw new WebApiException(StrUtil.format("未知授权类型({})", grantType));
        }
        if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
            throw new WebApiException("Token 接口不支持 implicit 授权模式");
        }


        // 1.2 校验客户端
        String[] clientIdAndSecret = OAuth2Utils.obtainBasicAuthorization(request);
        SysOauth2ClientDO client = oauth2ClientService.validOauthClient(clientIdAndSecret[0], null,
                grantType, scopes, redirectUri);

        // 2. 根据授权模式，获取访问令牌
        OAuth2AccessToken accessTokenDO;
        switch (grantTypeEnum) {
            case AUTHORIZATION_CODE:
                accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state);
                break;
            case PASSWORD:
                accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes);
                break;
            // 对应  Spring Security OAuth 的spring-security-oauth2包的 org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter
            // Spring Security OAuth的过滤器拦截请求，并使用DaoAuthenticationProvider进行client_id 和 client_secret校验
            case CLIENT_CREDENTIALS:
                accessTokenDO = oauth2GrantService.grantClientCredentials(client, client_id, client_secret, scopes);
                break;
            case REFRESH_TOKEN:
                accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId());
                break;
            default:
                throw new IllegalArgumentException("未知授权类型：" + grantType);
        }

        // 防御性检查
        Assert.notNull(accessTokenDO, "访问令牌不能为空");

        return ResResult.success(convertAccessToken(BeanUtil.copyProperties(accessTokenDO, AccessTokenInfo.class)));
    }



    @ApiOperation("退出登录")
    @DeleteMapping("/logout")
    public ResResult<Boolean> logout(@RequestBody OauthLogoutReqVO req, HttpServletRequest request) {
        String token = SecurityUtils.obtainHeaderToken(request);
        Authentication authentication = SecurityUtils.getAuthentication();
        if (authentication instanceof TokenAuthentication) {
            TokenAuthentication tokenAuthentication = (TokenAuthentication) authentication;
            String clientId = tokenAuthentication.getClientId();
            // 删除访问令牌
            return ResResult.success(oauth2GrantService.removeToken(clientId, token, req.getLogoutScope()));
        }
        return ResResult.success(true);

    }

    private OAuth2OpenAccessTokenRespVO convertAccessToken(OAuth2AccessToken accessToken) {
        OAuth2OpenAccessTokenRespVO vo = BeanUtil.copyProperties(accessToken, OAuth2OpenAccessTokenRespVO.class);
        vo.setExpiresIn(OAuth2Utils.getExpiresIn(accessToken.getExpiresTime()));
        vo.setTokenType(SecurityUtils.AUTHORIZATION_BEARER.toLowerCase());
        vo.setScope(OAuth2Utils.buildScopeStr(accessToken.getScopes()));
        return vo;
    }

    /**
     * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法
     */
    @PostMapping("/check-token")
    @ApiOperation(value = "校验访问令牌")
    @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "token", dataTypeClass = String.class)
    public ResResult<OAuth2OpenCheckTokenRespVO> checkToken(HttpServletRequest request,
                                                            @RequestParam("token") String token) {
        // 校验客户端
        String[] clientIdAndSecret = OAuth2Utils.obtainBasicAuthorization(request);
        oauth2ClientService.validOauthClient(clientIdAndSecret[0], null,
                null, null, null);
        // 校验令牌
        OAuth2AccessToken accessToken = oauth2TokenService.checkAccessToken(token);
        Assert.notNull(accessToken, "访问令牌不能为空");
        return ResResult.success(convertCheckAccessToken(accessToken));
    }

    private OAuth2OpenCheckTokenRespVO convertCheckAccessToken(OAuth2AccessToken accessToken) {
        OAuth2OpenCheckTokenRespVO vo = BeanUtil.copyProperties(accessToken, OAuth2OpenCheckTokenRespVO.class);
        vo.setExp(LocalDateTimeUtil.toEpochMilli(accessToken.getExpiresTime()) / 1000);
        vo.setScopes(accessToken.getScopes());
        return vo;
    }


    /**
     * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法
     */
    @GetMapping("/authorize")
    @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【获取】调用")
    @ApiImplicitParam(name = "clientId", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class)
    public ResResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId) {
        // 0. 校验用户已经登录。通过 Spring Security 实现

        // 1. 获得 Client 客户端的信息
        SysOauth2ClientDO client = oauth2ClientService.validOauthClient(clientId);
        // 2. 获得用户已经授权的信息
        List<SysOauth2ApproveDO> approves = oauth2ApproveService.getApproveList(SecurityUtils.getUserId(), getUserType(), clientId);
        // 拼接返回
        return ResResult.success(convertApproveScopes(client, approves));
    }

    private OAuth2OpenAuthorizeInfoRespVO convertApproveScopes(SysOauth2ClientDO client, List<SysOauth2ApproveDO> approves) {
        List<KeyValue<String, Boolean>> scopes = new ArrayList<>();

        Map<String, SysOauth2ApproveDO> approveMap = approves.stream()
                .collect(Collectors.toMap(SysOauth2ApproveDO::getScope, Function.identity(), (key1, key2) -> key1));
        client.getScopes().forEach(scope -> {
            SysOauth2ApproveDO approveDO = approveMap.get(scope);
            scopes.add(new KeyValue(scope, approveDO != null ? approveDO.getApproved() : false));
        });
        return new OAuth2OpenAuthorizeInfoRespVO(new OAuth2OpenAuthorizeInfoRespVO.Client(client.getClientName(), client.getClientLogo()), scopes);

    }

    /**
     * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法
     * <p>
     * 场景一：【自动授权 autoApprove = true】
     * 刚进入 sso.vue 界面，调用该接口，用户历史已经给该应用做过对应的授权，或者 OAuth2Client 支持该 scope 的自动授权
     * 场景二：【手动授权 autoApprove = false】
     * 在 sso.vue 界面，用户选择好 scope 授权范围，调用该接口，进行授权。此时，approved 为 true 或者 false
     * <p>
     * 因为前后端分离，Axios 无法很好的处理 302 重定向，所以和 Spring Security OAuth 略有不同，返回结果是重定向的 URL，剩余交给前端处理
     */
    @PostMapping("/authorize")
    @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【提交】调用")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),
            @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),
            @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 使用 Map<String, Boolean> 格式，Spring MVC 暂时不支持这么接收参数
            @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.baidu.com", dataTypeClass = String.class),
            @ApiImplicitParam(name = "auto_approve", required = true, value = "用户是否接受", example = "true", dataTypeClass = Boolean.class),
            @ApiImplicitParam(name = "state", example = "1", dataTypeClass = String.class)
    })
    public ResResult<String> approveOrDeny(@RequestParam("response_type") String responseType,
                                           @RequestParam("client_id") String clientId,
                                           @RequestParam(value = "scope", required = false) String scope,
                                           @RequestParam("redirect_uri") String redirectUri,
                                           @RequestParam(value = "auto_approve") Boolean autoApprove,
                                           @RequestParam(value = "state", required = false) String state) {


        Map<String, Boolean> scopes = JSONUtil.parseObj(scope).toBean(new TypeReference<Map<String, Boolean>>() {
        });


        scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
        // 0. 校验用户已经登录。通过 Spring Security 实现

        // 1.1 校验 responseType 是否满足 code 或者 token 值
        OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
        // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
        SysOauth2ClientDO client = oauth2ClientService.validOauthClient(clientId, null,
                grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);

        // 2.1 假设 approved 为 null，说明是场景一
        if (Boolean.TRUE.equals(autoApprove)) {
            // 如果无法自动授权通过，则返回空 url，前端不进行跳转
            if (!oauth2ApproveService.checkForPreApproval(SecurityUtils.getUserId(), getUserType(), clientId, scopes.keySet())) {
                return ResResult.success(null);
            }
            // 2.2 假设 approved 非 null，说明是场景二
        } else {
            // 如果计算后不通过，则跳转一个错误链接
            if (!oauth2ApproveService.updateAfterApproval(SecurityUtils.getUserId(), getUserType(), clientId, scopes)) {
                return ResResult.success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state,
                        "access_denied", "User denied access"));
            }
        }

        // 3.1 如果是 code 授权码模式，则发放 code 授权码，并重定向
        List<String> approveScopes = scopes.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey)
                .filter(Objects::nonNull).collect(Collectors.toList());
        if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
            return ResResult.success(getAuthorizationCodeRedirect(SecurityUtils.getUserId(),
                    SecurityUtils.getUser() == null ? null : SecurityUtils.getUser().getUsername(),
                    client, approveScopes, redirectUri, state));
        }
        // 3.2 如果是 token 则是 implicit 简化模式，则发送 accessToken 访问令牌，并重定向
        return ResResult.success(getImplicitGrantRedirect(SecurityUtils.getUserId(),
                SecurityUtils.getUser() == null ? null : SecurityUtils.getUser().getUsername(),
                client, approveScopes, redirectUri, state));
    }

    private String getAuthorizationCodeRedirect(Long userId, String userName, SysOauth2ClientDO client,
                                                List<String> scopes, String redirectUri, String state) {
        // 1. 创建 code 授权码
        String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, userName, getUserType(),
                client.getClientId(), scopes, redirectUri, state);
        // 2. 拼接重定向的 URL
        return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state);
    }

    private String getImplicitGrantRedirect(Long userId, String userName, SysOauth2ClientDO client,
                                            List<String> scopes, String redirectUri, String state) {
        // 1. 创建 access token 访问令牌
        OAuth2AccessToken accessTokenDO = oauth2GrantService.grantImplicit(userId, userName, getUserType(), client.getClientId(), scopes);
        Assert.notNull(accessTokenDO, "访问令牌不能为空");
        // 2. 拼接重定向的 URL
        // noinspection unchecked
        return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(),
                scopes, JSONUtil.parseObj(client.getAdditionalInformation()).toBean(new TypeReference<Map<String, Object>>() {
                }));
    }

    private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) {
        if (StrUtil.equals(responseType, "code")) {
            return OAuth2GrantTypeEnum.AUTHORIZATION_CODE;
        }
        if (StrUtil.equalsAny(responseType, "token")) {
            return OAuth2GrantTypeEnum.IMPLICIT;
        }
        throw new WebApiException("response_type 参数值只允许 code 和 token");
    }

    private Integer getUserType() {
        return UserTypeEnum.ADMIN.getValue();
    }

}
